Nuxt3 与 Vue3 的 Server api 全栈开发之路

您所在的位置:网站首页 supported api3怎么处理 Nuxt3 与 Vue3 的 Server api 全栈开发之路

Nuxt3 与 Vue3 的 Server api 全栈开发之路

2024-07-09 14:11| 来源: 网络整理| 查看: 265

Nuxt Server Api

阅读时长:15分钟

本文内容: 国内关于Nuxt3的资料太少了,而Nuxt3又发布了没有多久,导致资料太少。

本文目的:AI蓬勃发展,对于AI需求的网站,使用Nuxt开发是优选。

本文浓缩讲解了,对于前端开发,上手使用 Nuxt3,并一个人承担前后端开发的所有须知内容

[Nuxt3 + Vue3 + Typescript + TaiwindCss 企业级模板]: 即将发布

环境要求:

node : v16 +redis : v6 + export default defineEventHandler(async (event) => { } Nuxt3.js 官方文档Tailwindcss 文档Antdesign Vue 文档

在这里插入图片描述

起步 首先要把官方文档关于目录的解释看一遍: nuxt3 目录前端页面的开发,只需聚焦目录 pages,所有写法与传统Vue3一模一样.后端接口的开发,只需聚焦目录 server, 重点讲解服务端 目录 1. 目录 layout Nuxt会自动异步导入该目录下文件通常你至少有2种布局时才会用到这个目录,否则直接使用根目录下的 app.vue 即可一旦在该目录下创建了布局文件,例如 page.vue,那么此组件将自动加载,页面 Html 结构将被分为三部分: header,content,footer

例如::layout/page.vue

类似于 ElementPlus 框架的三部分:

Header Main Footer 2. 目录 composables Nuxt 只扫描 composables/ 目录的顶层文件目录 composables 通常放置全局方法,比如统一地 Api 接口,常量。

例如:composables/index.ts

import request from '@/utils/request' /** * get请求示例 * @param params * @returns */ export function httpGetResponse(params: any) { return request.get('/user-center/getUser', params) } /** * post请求示例 * @param data * @returns */ export function httpPostResponse(data: any) { return request.post('/user-center/updateUser', data) } // 合成百度语音,示例传递 blob 格式数据 export function getBaiduVoice(data: any): any { return request.post('/speech/baidu', data, { responseType: 'blob' }) } 对于业务组件,放在第二层目录即可,不会自动全局导入

例如:composables/business/myTabs.vue 例如 composables/business/myTabsUtil.ts

3. 目录 plugins 只有在 plugins/ 目录顶层(或任何子目录中文件名为 index 的文件)才会被注册为插件这个目录放置第三方插件文件。一旦文件创建并导出 defineNuxtPlugin 函数,那么会被 Nuxt 自动判定为插件,并加载

例如流行的UI库: plugins/antDesignVue.ts

// 1. 引入组件 import Antd from 'ant-design-vue'; // 2. 引入组件样式 import 'ant-design-vue/dist/antd.css'; export default defineNuxtPlugin((nuxt) => { nuxt.vueApp.use(Antd); })

例如: plugins/axios.ts

import axios from 'axios' export default defineNuxtPlugin((nuxtApp) => { return { provide: { axios, }, } }) 可以通过文件名添加后缀.server或.client来确保只有客户端或者服务端加载

例如UI库显然只有前端使用,服务端用不着: plugins/antDesignVue.client.ts

// 1. 引入组件 import Antd from 'ant-design-vue'; // 2. 引入组件样式 import 'ant-design-vue/dist/antd.css'; export default defineNuxtPlugin((nuxt) => { nuxt.vueApp.use(Antd); }) 4. 目录 store 这个目录显然是存储数据的,使用的 pinia

例如: store/modules/useExample.ts

export interface ICounterState { count: number } export const useExample = definePiniaStore('example', { state: (): ICounterState => ({ count: 0, }), actions: { increment() { this.count++ }, decrement() { this.count-- }, reset() { this.count = 0 }, increment2x() { this.count *= 2 }, }, }) import { useExample} from "@/store/modules/useExample"; // 获取 const exampleStore = useExample(); let count = exampleStore.count; console.log(count) exampleStore.increment(); console.log(exampleStore.count) 5. 目录 assets

这个目录用于存放静态文件: image,js, css, 视频,音频,字体 在这里插入图片描述

6. 目录 public 存放不会变动的公共文件。例如:robots.txt,favicon.ico 7. 目录 utils 存放工具类

例如utils/request.ts

/* * @Date: 2023-06-18 00:16:29 * @LastEditors: ifredom [email protected] * @LastEditTime: 2023-08-29 13:46:46 * @FilePath: \first-nuxt\utils\request.ts */ import { UseFetchOptions } from 'nuxt/app' import { AjaxResult, Methods } from '../types' class HttpService { request( url: string, method: Methods, data: any, options?: UseFetchOptions ) { const $config = useRuntimeConfig() const requestUrl = url.includes('https') || url.includes('http') ? url : $config.app.BASE_URL + url const newOptions: UseFetchOptions = { baseURL: requestUrl, method, ...options, } if (method === 'GET' || method === 'DELETE') { newOptions.params = data } if (method === 'POST' || method === 'PUT') { newOptions.body = data } return new Promise((resolve, reject) => { useFetch(requestUrl, newOptions) .then((response) => { const { data, error } = response if (error && error.value) { console.log('toast 网络错误') const errorMessage = JSON.stringify(error.value) message.error(errorMessage) reject(error.value) return } if (newOptions.responseType === 'blob') { resolve(response.data.value) return } const res = data.value as AjaxResult if (res && res.code === 0) { resolve(res.data) } else if (res && res.msg) { if (res.code === 40000) { // 会话过期跳登录 setTimeout(() => { navigateTo('/login') }, 1000) return } reject(res.data) } resolve(res) }) .catch((error) => { reject(error) }) }) } get(url: string, params?: any, options?: UseFetchOptions) { return this.request(url, 'GET', params, options) } post(url: string, data?: any, options?: UseFetchOptions) { return this.request(url, 'POST', data, options) } put(url: string, data: any, options?: UseFetchOptions) { return this.request(url, 'PUT', data, options) } delete(url: string, params: any, options?: UseFetchOptions) { return this.request(url, 'DELETE', params, options) } } const httpRequest = new HttpService() export default httpRequest import request from '@/utils/request' /** * get请求示例 * @param params * @returns */ export function httpGetResponse(params: any) { return request.get('/user-center/getUser', params) } /** * post请求示例 * @param data * @returns */ export function httpPostResponse(data: any) { return request.post('/user-center/updateUser', data) } 8 错误error.vue Vue渲染生命周期中的错误(SSR + SPA)API或Nitro服务器生命周期中的错误服务器和客户端启动错误(SSR + SPA)

例如,定义全局错误抓取,将错误上传至服务器 plugins/errorlog.ts

export default defineNuxtPlugin((nuxt) => { nuxt.vueApp.config.errorHandler = (error, context) => { // 抓取全局错误,上传至服务器 const message = error + context $http.post('erroor-log',message) } })

目录 server 比较复杂,单独讲解

目录:server

服务端 Server Api 设置(H3)

Nuxt3 的服务端使用 Nitro 构建,与 H3 这个库紧密结合,你甚至可以认为服务端的接口都使用 H3 来编写

# 安装 pnpm add h3 1. 设置Http状态 import { H3Event } from "h3" export default defineEventHandler(async (event: H3Event) => { setResponseStatus(event, 204) return { data: '', code: 0, msg: 'success' } } 2. 设置 Cookie const AUTH_COOKIE_NAME = '__session' const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1_000 export default defineEventHandler(async (event: H3Event) => { const cookie = 'ffkeifpoeapoifm321654' setCookie(event, AUTH_COOKIE_NAME, cookie, { maxAge: AUTH_COOKIE_MAX_AGE, secure: true, httpOnly: true, path: '/', sameSite: 'lax', }) return { data: '', code: 0, msg: 'success' } } 3. 存储 interface Query { region?: 'zh-CN' | 'en-US' | 'ja-JP' | 'en-AU' | 'en-UK' | 'de-DE' | 'en-NZ' | 'en-CA' } export default defineEventHandler(async (event) => { const { type = 'img', region = 'zh-CN' } = getQuery(event) if (type === 'img') { const cache = await useStorage('cache').getItem('bing-wallpaper') if (cache) return await sendRedirect(event, cache, 302) } // https://github.com/zkeq/Bing-Wallpaper-Action const data = await (await fetch(`https://raw.onmicrosoft.cn/Bing-Wallpaper-Action/main/data/${region}_all.json`)).json() if (type === 'img') { const url = `https://bing.com${data.data[0].url}` useStorage('cache').setItem('cache:bing-wallpaper', url, { ttl: getTodayRemainMillisecond() }) return await sendRedirect(event, url, 302) // event.node.res.setHeader('Content-Type', 'image/png;charset=utf-8') // return Buffer.from(await (await fetch(url)).arrayBuffer()) } else { event.context.cache = { ttl: TimeUnitMap.hour } return data } }) 4. 验证 import { z, useValidatedBody } from 'h3-zod' export default defineEventHandler(async event => { const body = await useValidatedBody( event, z.object({ userId: z.string(), }) ) }) 5. 返回Error export default defineEventHandler(async (event) => { try { const cookie = getCookie(event, 'refresh_token') as Token; await deleteRefreshToken(cookie); return { cookie, }; } catch (error) { sendError( event, createError({ statusCode: 400, statusMessage: 'username or password is invalid', }) ); } sendRefreshToken(event, ''); return { message: 'Logout successful', }; }); 6. 设置返回数据类型 1. 返回 audio 类型数据

服务端设置 header

// server/api/speech.ts export default defineEventHandler(async (event: H3Event) => { try { // 获取音频数据 const audioBlob = await fetch_to_audio() let size = 0; if (audioBlob instanceof Blob) { size = audioBlob.size } setResponseHeader(event, 'Accept-Ranges', 'bytes') setResponseHeader(event, 'Content-Type', 'audio/wav') setResponseHeader(event, 'Content-Length', size) return audioBlob; } catch (error) { sendError( event, createError({ statusCode: 400, statusMessage: 'username or password is invalid', }) ); } })

前端(客户端)设置数据返回类型为blob

const createAudio = async () => { const response: any = await axios({ url: '/api/speech', method: "POST", data: { content: '123456', }, responseType: "blob" }) const { data: arrayBuffer } = response console.log('------收到服务端数据------', response) const box = document.querySelector("#audio-box") as HTMLElement const audioElement = document.createElement("audio"); //创建标签 audioElement.autoplay = true audioElement.controls = true audioElement.src = URL.createObjectURL(arrayBuffer); // 指定链接 box.appendChild(audioElement) } 音频: 2. 返回 image 图片类型数据

服务端设置 header

// server/api/image.ts export default defineEventHandler(async (event: H3Event) => { try { // 获取图片 const image = await fetch_to_image() let size = 0; if (audioBlob instanceof Blob) { size = audioBlob.size } setHeader(e, 'Content-Type', 'image/png') return image } catch (error) { sendError( event, createError({ statusCode: 400, statusMessage: 'username or password is invalid', }) ); } }) 3. 返回 File 图片类型数据

以文件上传为例

前端页面: pages/login.vue

const $config = useRuntimeConfig() const axios = useNuxtApp().$axios const BaseUrl:string = "http://127.0.0.1:9000/api/v1/login" const testServerApi = async () => { // 图片: File格式 const imageFile = imageFiles.value as File // 音频:Blob格式 const audioFileBlob = audioBlob.value as Blob // 音频Blob转file const newAudioFileBlob = new Blob([audioFileBlob], { type: 'audio/wav' }) const audioFile = new File([newAudioFileBlob], new Date().getTime() + '.wav') if (!imageFile) { return } // 创建formData const formData = new FormData() formData.append('image', imageFile) formData.append('audio', audioFile) // 方式一:使用 useFetch() 推荐 (文末有完整封装) const res: any = await useFetch($config.BaseUrl, formData) // 方式二:使用 axios // const response = await axios({ // method: 'POST', // url: 'http://127.0.0.1:9000/api/v1/login', // data: formData, // headers, // }) console.log(res) } 点击

服务端接口: server/api/user/login.ts

import { H3Event, MultiPartData } from 'h3' import { FormData } from 'node-fetch-native' import { AjaxResult } from '../../../types' export default defineEventHandler( async (event: H3Event): Promise => { const response = await send_image_audio_to_remote(event) return { data: response, code: 0, msg: 'success', } } ) const send_image_audio_to_remote = async (event: H3Event) => { // 重点!!!前端传输的文件使用此方法获取 const form = await readMultipartFormData(event) try { if (form) { const imageMultiPartData: MultiPartData = form[0] const audioMultiPartData: MultiPartData = form[1] // 服务端,也可以继续发起请求,比如百度云的语音服务 const formData = new FormData() formData.append('image', new Blob([imageMultiPartData.data])) formData.append('audio', new Blob([audioMultiPartData.data])) const responseData: any = await create_access_token() console.log('=========responseData==============') console.log(responseData) // 返回 return true } return '' } catch (error) { sendError( event, createError({ statusCode: 400, statusMessage: 'server is invalid', }) ) } } /** * 百度云语音服务 */ const create_access_token = async () => { const { awesome } = useAppConfig() const client_id = awesome.baiduTTS.client_id const client_secret = awesome.baiduTTS.client_secret const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}` const option = { grant_type: 'client_credentials', client_id, client_secret, } const headers = { 'Content-Type': 'application/json', Accept: 'application/json', } const res: BaiduTTSAccessToken = await $fetch(url, { method: 'POST', headers, body: option, }) if (res.error_code) { return res.error_code + '' } return res.access_token } export interface AjaxResult { data?: any code: number msg: string } SQL使用 1. postgres 数据库 import { createPool, sql } from '@vercel/postgres' async function seed() { const createTable = await sql` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, image VARCHAR(255), "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); ` console.log(`Created "users" table`) const users = await Promise.all([ sql` INSERT INTO users (name, email, image) VALUES ('Guillermo Rauch', '[email protected]', 'https://pbs.twimg.com/profile_images/1576257734810312704/ucxb4lHy_400x400.jpg') ON CONFLICT (email) DO NOTHING; `, sql` INSERT INTO users (name, email, image) VALUES ('Lee Robinson', '[email protected]', 'https://pbs.twimg.com/profile_images/1587647097670467584/adWRdqQ6_400x400.jpg') ON CONFLICT (email) DO NOTHING; `, sql` INSERT INTO users (name, email, image) VALUES ('Steven Tey', '[email protected]', 'https://pbs.twimg.com/profile_images/1506792347840888834/dS-r50Je_400x400.jpg') ON CONFLICT (email) DO NOTHING; `, ]) console.log(`Seeded ${users.length} users`) return { createTable, users, } } export default defineEventHandler(async () => { const startTime = Date.now() const db = createPool() try { const { rows: users } = await db.query('SELECT * FROM users') const duration = Date.now() - startTime return { users: users, duration: duration, } } catch (error) { // @ts-ignore if (error?.message === `relation "users" does not exist`) { console.log( 'Table does not exist, creating and seeding it with dummy data now...' ) // Table is not created yet await seed() const { rows: users } = await db.query('SELECT * FROM users') const duration = Date.now() - startTime return { users: users, duration: duration, } } else { throw error } } }) 2. mysql2 安装 Antdesign Vue两种方式 方式一,模块方式 # Using pnpm pnpm add -D @ant-design-vue/nuxt

nuxt.config.ts

export default defineNuxtConfig({ modules: [ '@ant-design-vue/nuxt' ], antd:{ // Options } })

使用

const handleMessage = () => { message.info("This is a normal message"); } button 方式二,插件方式 # Using pnpm pnpm add -D ant-design-vue

在plugins文件中新建 awesome.ts

// 1. 引入组件 import Antd from 'ant-design-vue'; // 2. 引入组件样式 import 'ant-design-vue/dist/antd.css'; export default defineNuxtPlugin((nuxt) => { nuxt.vueApp.use(Antd); })

使用

const handleMessage = () => { message.info("This is a normal message"); } button Server Storage 服务端存储

Nuxt的服务端通常只作为转发/代理中间层,因此数据存储使用轻量的 Redis 存储

操作系统安装RedisRedis集成有两种方式 方式一:通过手写代码集成

server/plugins/storage.ts

import redisDriver from 'unstorage/drivers/redis' export default defineNitroPlugin(() => { const storage = useStorage() // Dynamically pass in credentials from runtime configuration, or other sources const driver = redisDriver({ base: 'redis', host: useRuntimeConfig().redis.host, port: useRuntimeConfig().redis.port, /* other redis connector options */ }) // Mount driver storage.mount('redis', driver) })

nuxt.config.ts

export default defineNuxtConfig({ runtimeConfig: { redis: { // Default values host: '', port: 0, /* other redis connector options */ } } }) 方式二:通过nuxt.内置的nitro服务,nitro.storage

nuxt.config.ts

export default defineNuxtConfig({ nitro: { storage: { 'redis': { driver: 'redis', /* redis connector options */ port: 6379, // Redis port host: "127.0.0.1", // Redis host username: "", // needs Redis >= 6 password: "", db: 0, // Defaults to 0 tls: {} // tls/ssl } } } })

集成后使用:

/server/api/test/setStorage.ts 存数据

export default defineEventHandler(async (event): Promise => { const redis = useStorage('first') redis.setItem('redis:test', 'Nuxt is Awesome!') console.log('======save success======') return { data: body, code: 0, msg: 'success', } })

/server/api/test/getStorage.ts 取数据

export default defineEventHandler(async (event): Promise => { const redis = useStorage('first') const data = await redis.getItem('redis:test') console.log('======get success======', data) return { data, code: 0, msg: 'success', } }) 其他 谷歌&github 登录 import { NuxtAuthHandler } from "#auth"; import GithubProvider from "next-auth/providers/github"; import GoogleProvider from "next-auth/providers/google"; const nuxtAuthHandler = NuxtAuthHandler({ secret: useRuntimeConfig().authSecret, providers: [ // @ts-ignore GithubProvider.default({ clientId: useRuntimeConfig().githubClientId, clientSecret: useRuntimeConfig().githubClientSecret, httpOptions: { timeout: 10000, }, }), // @ts-ignore GoogleProvider.default({ clientId: useRuntimeConfig().googleClientId, clientSecret: useRuntimeConfig().googleClientSecret, }), ], }); export default defineEventHandler(async (event) => { const result = await nuxtAuthHandler(event); const headerCookies = event.node.res.getHeader("set-cookie"); if (headerCookies && typeof headerCookies === "string") { const sessionToken = headerCookies .split(/,(?!\s)/) .find((v) => v.includes("session-token")); if (sessionToken) { event.node.res.removeHeader("set-cookie"); event.node.res.setHeader("set-cookie", sessionToken); } } return result; }); 服务端请求封装$useFetch import { UseFetchOptions } from 'nuxt/app' import { AjaxResult, Methods } from '../types' /* * @Date: 2023-08-18 00:16:29 * @LastEditors: ifredom [email protected] * @LastEditTime: 2023-08-29 13:46:46 * @FilePath: \first-nuxt\utils\request.ts */ class HttpService { request( url: string, method: Methods, data: any, options?: UseFetchOptions ) { const $config = useRuntimeConfig() const requestUrl = url.includes('https') || url.includes('http') ? url : $config.app.BASE_URL + url const newOptions: UseFetchOptions = { baseURL: requestUrl, method, ...options, } if (method === 'GET' || method === 'DELETE') { newOptions.params = data } if (method === 'POST' || method === 'PUT') { newOptions.body = data } return new Promise((resolve, reject) => { useFetch(requestUrl, newOptions) .then((response) => { const { data, error } = response if (error && error.value) { console.log('toast 网络错误') reject(error.value) return } if (newOptions.responseType === 'blob') { resolve(response.data.value) return } const res = data.value as AjaxResult if (res && res.code === 0) { resolve(res.data) } else if (res && res.msg) { if (res.code === 40000) { // 会话过期跳登录 setTimeout(() => { navigateTo('/login') }, 1000) return } reject(res.data) } resolve(res) }) .catch((error) => { reject(error) }) }) } get(url: string, params?: any, options?: UseFetchOptions) { return this.request(url, 'GET', params, options) } post(url: string, data?: any, options?: UseFetchOptions) { return this.request(url, 'POST', data, options) } put(url: string, data: any, options?: UseFetchOptions) { return this.request(url, 'PUT', data, options) } delete(url: string, params: any, options?: UseFetchOptions) { return this.request(url, 'DELETE', params, options) } } const httpRequest = new HttpService() export default httpRequest

types.ts类型文件

export type Methods = "GET" | "POST" | "DELETE" | "PUT"; export interface AjaxResult { data?: any code: number msg: string }

使用示例:

/** * get请求示例 * @param params * @returns */ export function httpGetResponse(params: any) { return request.get('/user-center/getUser', params) } /** * post请求示例 * @param data * @returns */ export function httpPostResponse(data: any) { return request.post('/user-center/updateUser', data) } /** * post请求: 携带请求头 * @param data * @returns */ export function httpPostWithHeader(data: any): any { return request.post('/user-center/update_Avator', data, { responseType: 'blob' }) } 参考文档

参考 API

H3 API documentH3 githubH3 1.8 API latest intro

参考例子

例子About基于Nuxt3 + Vite3 + Vue3 + UnoCSS搭建的API接口服务网站参考项目audio返回数据Base64 、Blob、File之间的相互转换NUXT3 server/api params cause 500 error on production)How to create an AudioBuffer from a BlobHow to return image with server apiHow to return audio data with server api

------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3